Repository navigation
Bind extension methods to the extended type so they are callable from the instance - #150
jhonabreul wants to merge 4 commits into
Conversation
… the instance C# extension methods were only reachable from Python through their static class, so instance-style calls such as order_status.is_open() failed with AttributeError. Extension methods declared in non-framework assemblies are now indexed during the assembly scan and bound as members of the type they extend, including interface targets on implementing classes. The binder passes the instance as the first argument when an extension method is called through it. Real members with the same name win, and open generic targets are skipped since the binder cannot infer them from the instance.
Targets made only of framework types are shared by countless classes: extensions on object reached every CLR object, and Accord.Math alone added ~600 methods to each primitive array type. Across Lean's assemblies the index shrinks from 16487 to 653 methods. Such extensions keep their static form. Assemblies without the compiler's assembly-level [Extension] marker are skipped up front, as C# does, and binding a class only walks its interfaces when an interface target was indexed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-up: pushed 86be7c6 and e2af525Where:
WhatExtension methods are now bound to an instance only when their target involves a non-framework type: the type itself, an array's element type, or a generic argument.
Extensions that are not bound still work through their static class, e.g. WhyOnly the declaring assembly was checked against the framework list, not the extended type. Measured over the 88 assemblies in Lean's
How
Result: the index goes from 16,487 to 653 methods. The one-time Behaviour change: the existing Hiding helpers with
|
…form [EditorBrowsable(EditorBrowsableState.Never)] hides a member from IntelliSense while keeping it callable. Binding such extension methods to the instance would advertise them anyway, so those methods, and all the extension methods of a class marked so, keep their static form only. It lets a library hide plumbing helpers, like Lean's IAlgorithm extensions, from the instance with a standard attribute. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
What does this implement/fix? Explain your changes.
C# extension methods were only reachable from Python through their static class, so the instance-style call every C# example uses failed:
Cause:
ClassManagerbuilds a type's Python class from its real members only, and extension methods are static methods on another type.The change:
ExtensionMethodsindexes the public extension methods of each scanned assembly by the type they extend, insideAssemblyManager.ScanAssembly. Framework assemblies andPython.Runtimeare skipped so LINQ is not grafted onto every collection.ClassManager.GetClassInfobinds the extensions of the type and of the interfaces it introduces as members, so snake-case aliases come for free. A real member with the same name wins, as in C#.MethodBinder.Bindtreats an extension called through an instance like a bound operator: it matches the Python arguments against the remaining parameters and prepends the instance.this T,this IEnumerable<T>) and by-ref targets are skipped because the binder cannot infer them from the instance.AssemblyManager.Initializeso a re-initialized engine rebuilds it.Limits: targets converted to Python natives (
string,decimal,DateTime,double) cannot be bound, and their extensions stay available through the static class.Does this close any currently open issues?
No.
Any other comments?
Tests, in
TestExtensionMethods:List<int>) is bound; open generic target is not.string) is not bound but its static form works.